package hudson.plugins.violations; import static java.util.logging.Level.SEVERE; import hudson.model.HealthReport; import hudson.model.Result; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Project; /** cannot be used in a slave import hudson.maven.MavenModule; /**/ import hudson.plugins.violations.hudson.AbstractViolationsBuildAction; /** cannot be used in a slave import hudson.plugins.violations.hudson.maven.*; /**/ import hudson.plugins.violations.model.BuildModel; import hudson.plugins.violations.model.FileModel; import hudson.plugins.violations.model.Suppression; import hudson.plugins.violations.parse.BuildModelParser; import hudson.plugins.violations.parse.ParseXML; import hudson.plugins.violations.render.FileModelProxy; import hudson.plugins.violations.render.NoViolationsFile; import hudson.plugins.violations.util.HelpHudson; import hudson.plugins.violations.util.RecurDynamic; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * This contains the report for the violations of a particular build. */ public class ViolationsReport implements Serializable { private static final Logger LOG = Logger.getLogger(ViolationsReport.class.getName()); private AbstractBuild<?, ?> build; private ViolationsConfig config; private final Map<String, Integer> violations = new TreeMap<String, Integer>(); private final Map<String, TypeSummary> typeSummaries = new TreeMap<String, TypeSummary>(); private transient WeakReference<BuildModel> modelReference; /** * Set the build. * * @param build * the current build. */ public void setBuild(AbstractBuild<?, ?> build) { this.build = build; } /** * Get the build. * * @return the build. */ public AbstractBuild<?, ?> getBuild() { return build; } /** * Set the config. * * @param config * the config. */ public void setConfig(ViolationsConfig config) { this.config = config; } /** * Get the config. * * @return the config. */ public ViolationsConfig getConfig() { return config; } /** * Get the violation counts for the build. * * @return a map of type to count. */ public Map<String, Integer> getViolations() { return violations; } /** * Get the overall health for the build. * * @return the health report, null if there are no counts. */ public HealthReport getBuildHealth() { List<HealthReport> reports = getBuildHealths(); HealthReport ret = null; for (HealthReport report : reports) { ret = HealthReport.min(ret, report); } return ret; } /** * Get a health report for each type. * * @return a list of health reports. */ public List<HealthReport> getBuildHealths() { List<HealthReport> ret = new ArrayList<HealthReport>(); for (String type : config.getTypeConfigs().keySet()) { HealthReport health = getHealthReportFor(type); if (health != null) { ret.add(health); } } return ret; } /** * Get the health for a particulat type. * * @param type * the type to get the health for. * @return the health report. */ public HealthReport getHealthReportFor(String type) { Integer count = violations.get(type); if (count == null || config.getTypeConfigs() == null) { return null; } int h = config.getTypeConfigs().get(type).getHealthFor(count); if (h < 0) { return new HealthReport(0, Messages._ViolationsReport_NoReport(type)); } else { return new HealthReport(h, Messages._ViolationsReport_ViolationsCount(type, count)); } } /** * Get the detailed model for the build. This is lazily build from an xml * created during publisher action. * * @return the build model. */ public BuildModel getModel() { BuildModel model = null; if (modelReference != null) { model = modelReference.get(); if (model != null) { return model; } } File xmlFile = new File(build.getRootDir(), MagicNames.VIOLATIONS + "/" + MagicNames.VIOLATIONS + ".xml"); try { model = new BuildModel(xmlFile); ParseXML.parse(xmlFile, new BuildModelParser().buildModel(model)); } catch (Exception ex) { LOG.log(Level.WARNING, "Unable to parse " + xmlFile, ex); return null; } modelReference = new WeakReference<BuildModel>(model); return model; } /** * Get the file model proxt for a file name. * * @param name * the name to use. * @return the file model proxy. */ public FileModelProxy getFileModelProxy(String name) { BuildModel model = getModel(); if (model == null) { return null; } return model.getFileModelMap().get(name); } /** * This gets called to display a particular violation file report. * * @param token * the current token in the path being parsed. * @param req * the http/stapler request. * @param rsp * the http/stapler response. * @return an object to handle the token. */ public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { // System.out.println("LOOKING for " + req.getRestOfPath()); String name = req.getRestOfPath(); if (name.equals("")) { return null; } if (name.startsWith("/")) { name = name.substring(1); } FileModelProxy proxy = getFileModelProxy(name); if (proxy != null) { return new RecurDynamic("", name, proxy.build(build).contextPath("")); } else { return new RecurDynamic("", name, new NoViolationsFile(name, build)); } } /** * get the configuration for this job. * * @return the configuration of the job. */ public ViolationsConfig getLiveConfig() { AbstractProject<?, ?> abstractProject = build.getProject(); if (abstractProject instanceof Project) { Project project = (Project) abstractProject; ViolationsPublisher publisher = (ViolationsPublisher) project.getPublisher(ViolationsPublisher.DESCRIPTOR); return publisher == null ? null : publisher.getConfig(); } return null; } /** * Add a suppression to the set of suppressions. * * @param suppression * the suppression to add. */ public void addSuppression(Suppression suppression) throws IOException { ViolationsConfig config = getLiveConfig(); if (config != null) { config.getSuppressions().add(suppression); ((AbstractProject) build.getParent()).save(); } } /** * Remove a suppression to the set of suppressions. * * @param suppression * the suppression to remove. */ public void removeSuppression(Suppression suppression) throws IOException { ViolationsConfig config = getLiveConfig(); if (config != null) { config.getSuppressions().remove(suppression); ((AbstractProject) build.getParent()).save(); } } /** * Get a map of type to type summary report. * * @return a map. */ public Map<String, TypeSummary> getTypeSummaries() { return typeSummaries; } /** * Get a type summary for a particular type. * * @param type * the violation type. * @return the type summary. */ public TypeSummary getTypeSummary(String type) { TypeSummary ret = typeSummaries.get(type); if (ret == null) { ret = new TypeSummary(); typeSummaries.put(type, ret); } return ret; } /** * Get a map of type to type reports. * * @return a map of type to type reports. */ public Map<String, TypeReport> getTypeReports() { Map<String, TypeReport> ret = new TreeMap<String, TypeReport>(); for (String t : violations.keySet()) { int c = violations.get(t); HealthReport health = getHealthReportFor(t); ret.put(t, new TypeReport(t, health.getIconUrl(), c)); } return ret; } /** * Graph this report. Note that for some reason, yet unknown, hudson seems * to pick an in memory ViolationsReport object and not the report for the * build. (Reason may be related to the fact that serialized builds may not * be the same as in-memory builds). Need to find the correct build from the * URI. * * @param req * the request paramters * @param rsp * the response. * @throws IOException * if there is an error writing the graph. */ public void doGraph(StaplerRequest req, StaplerResponse rsp) throws IOException { AbstractBuild<?, ?> tBuild = build; int buildNumber = HelpHudson.findBuildNumber(req); if (buildNumber != 0) { tBuild = build.getParent().getBuildByNumber(buildNumber); if (tBuild == null) { tBuild = build; } } AbstractViolationsBuildAction r = tBuild.getAction(AbstractViolationsBuildAction.class); if (r == null) { return; } r.doGraph(req, rsp); } /** * Get the string number for a particular type. * * @param t * the type * @return the string - a number for a value type, "" for not found and * "No reports" for < 0. */ public String getNumberString(String t) { Integer v = violations.get(t); if (v == null) { return ""; } if (v < 0) { return "<span style='color:red'>No reports</span>"; } return "" + v; } /** * Get the icon for a type. * * @param t * the type * @return the icon name. */ public String getIcon(String t) { violations.get(t); HealthReport h = getHealthReportFor(t); if (h == null) { return null; } return h.getIconUrl(); } /** * Report class for a particular type. */ public static class TypeReport { private final String type; private final String icon; private final int number; /** * Create the report class for a type. * * @param type * the violation type. * @param icon * the health icon to display. * @param number * the number of violations. */ public TypeReport(String type, String icon, int number) { this.type = type; this.icon = icon; this.number = number; } /** * Get the violation type. * * @return the violation type. */ public String getType() { return type; } /** * Get the health icon to display. * * @return the health icon. */ public String getIcon() { return icon; } /** * Get the number of violations. * * @return the number. */ public int getNumber() { return number; } /** * Get the number of violations as a string. * * @return the number if >= 0 othersise an error string. */ public String getNumberString() { if (number >= 0) { return "" + number; } else { return "<span style='color:red'>No reports</span>"; } } } /** * Get the previous ViolationsReport * * @return the previous report if present, null otherwise. */ public ViolationsReport previous() { return findViolationsReport(build.getPreviousBuild()); } /** * Get the number of violations for a particular type. * * @param type * the violation type. * @return the number of violations. */ public int typeCount(String type) { if (getModel() == null) { return 0; } return getModel().getTypeCountMap().get(type).getCount(); } /** * Get the number of files in violation for a particular type. * * @param type * the violation type. * @return the number of files. */ public int fileCount(String type) { if (getModel() == null) { return 0; } return getModel().getTypeCountMap().get(type).getNumberFiles(); } /** * Get the number of violations of a type for a file. * * @param type * the type in question. * @param filename * the name of the file. * @return the number found (0 for none and for failures). */ public int violationCount(String type, String filename) { FileModelProxy proxy = getFileModelProxy(filename); if (proxy == null) { return 0; } FileModel fileModel = proxy.getFileModel(); if (fileModel == null) { return 0; } FileModel.LimitType limit = fileModel.getLimitTypeMap().get(type); if (limit == null) { return 0; } return limit.getNumber(); } /** * Get the unstable status for this report. * * @return true if one of the violations equals or exceed the unstable * threshold for that violations type. */ private boolean isUnstable() { for (String t : violations.keySet()) { int count = violations.get(t); Integer unstableLimit = config.getTypeConfigs().get(t).getUnstable(); if (unstableLimit == null) { continue; } if (count >= unstableLimit) { return true; } } return false; } /** * Get the failed status for this report. * * @return true if one of the violations equals or exceed the failed * threshold of that violations type. */ private boolean isFailed() { for (String t : violations.keySet()) { int count = violations.get(t); Integer failCount = config.getTypeConfigs().get(t).getFail(); if (failCount == null) { continue; } if (count >= failCount) { return true; } } return false; } /** * Set the unstable/failed status of a build based on this violations * report. */ public void setBuildResult() { if (isFailed()) { build.setResult(Result.FAILURE); return; } if (isUnstable()) { try { build.setResult(Result.UNSTABLE); } catch (IllegalStateException e) { LOG.log(SEVERE, "Cannot set build to unstable for maven jobs, only for freestyle jobs. Issue: JENKINS-25406", e); } } } private static final long serialVersionUID = 1L; public static ViolationsReport findViolationsReport(AbstractBuild<?, ?> b) { for (; b != null; b = b.getPreviousBuild()) { if (b.getResult().isWorseOrEqualTo(Result.FAILURE)) { continue; } AbstractViolationsBuildAction action = b.getAction(AbstractViolationsBuildAction.class); if (action == null || action.getReport() == null) { continue; } ViolationsReport ret = action.getReport(); ret.setBuild(b); return ret; } return null; } public static ViolationsReportIterator iteration(AbstractBuild<?, ?> build) { return new ViolationsReportIterator(build); } public static class ViolationsReportIterator implements Iterator<ViolationsReport>, Iterable<ViolationsReport> { private AbstractBuild<?, ?> curr; public ViolationsReportIterator(AbstractBuild<?, ?> curr) { this.curr = curr; } @Override public Iterator<ViolationsReport> iterator() { return this; } @Override public boolean hasNext() { return findViolationsReport(curr) != null; } @Override public ViolationsReport next() { ViolationsReport ret = findViolationsReport(curr); if (ret != null) { curr = ret.getBuild().getPreviousBuild(); } return ret; } @Override public void remove() { throw new UnsupportedOperationException(); } } }